package com.http.nio.server;

import com.http.nio.core.*;
import com.http.nio.core.config.ServerConfig;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.*;

/**
 * @author YL
 */
public class NioServer {

    private final Integer port;

    private volatile boolean running = false;

    /**
     * 单例
     */
    private static volatile NioServer server;

    private final CloudService cloudService;

    /**
     * nio tcp 通道
     */
    private ServerSocketChannel serverSocketChannel;

    /**
     * 缓冲区
     * allocateDirect 系统缓冲区
     * allocate jvm 直接缓冲区分配快
     * <p>
     * 正常情况下，JVM创建一个缓冲区的时候，实际上做了如下几件事：
     * <p>
     * 1.JVM确保Heap区域内的空间足够，如果不够则使用触发GC在内的方法获得空间;
     * 2.获得空间之后会找一组堆内的连续地址分配数组, 这里需要注意的是，在物理内存上，这些字节是不一定连续的;
     * 3.对于不涉及到IO的操作，这样的处理没有任何问题，但是当进行IO操作的时候就会出现一点性能问题.
     * <p>
     * 所有的IO操作都需要操作系统进入内核态才行，而JVM进程属于用户态进程, 当JVM需要把一个缓冲区写到某个Channel或Socket的时候，需要切换到内核态.
     * <p>
     * 而内核态由于并不知道JVM里面这个缓冲区存储在物理内存的什么地址，并且这些物理地址并不一定是连续的(或者说不一定是IO操作需要的块结构)，所以在切换之前JVM需要把缓冲区复制到物理内存一块连续的内存上, 然后由内核去读取这块物理内存，整合成连续的、分块的内存.
     * 也就是说如果我们这个时候用的是非直接缓存的话，我们还要进行“复制”这么一个操作，而当我们申请了一个直接缓存的话，因为他本是就是一大块连续地址，我们就可以直接在它上面进行IO操作，省去了“复制”这个步骤
     * <p>
     * 当然缺点也是有的，他的分配和释放都比较昂贵，相对于非直接缓存而言
     * <p>
     * <p>
     * 所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据, 或者从键盘获取输入等. 而唯一可以做这些事情的就是操作系统, 所以此时程序就需要先操作系统请求以程序的名义来执行这些操作.
     * <p>
     * 这时需要一个这样的机制: 用户态程序切换到内核态, 但是不能控制在内核态中执行的指令
     * <p>
     * 这种机制叫系统调用, 在CPU中的实现称之为陷阱指令(Trap Instruction)
     * <p>
     * 他们的工作流程如下:
     * <p>
     * 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈(stack frame), 以此表明需要操作系统提供的服务.
     * 用户态程序执行陷阱指令
     * CPU切换到内核态, 并跳到位于内存指定位置的指令, 这些指令是操作系统的一部分, 他们具有内存保护, 不可被用户态程序访问
     * 这些指令称之为陷阱(trap)或者系统调用处理器(system call handler). 他们会读取程序放入内存的数据参数, 并执行程序请求的服务
     * 系统调用完成后, 操作系统会重置CPU为用户态并返回系统调用的结果
     */
    private ByteBuffer readBuffer = ByteBuffer.allocate(8192);

    /**
     * 线程池
     */
    private static final ExecutorService REQUEST_WORK = new ThreadPoolExecutor(
            10,
            200,
            30,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(1000),
            (r) -> new Thread(r, "NioServer")
    );

    private NioServer(Integer port, CloudService cloudService) {
        this.port = port;
        this.cloudService = cloudService;
    }

    public static NioServer getServerInstance(ServerConfig config) {
        if (server == null) {
            synchronized (NioServer.class) {
                if (server == null) {
                    server = new NioServer(config.getPort(), config.getCloudService());
                }
            }
        }
        return server;
    }

    public synchronized void start() {
        if (running) {
            System.out.println("服务器已经启动");
            return;
        }
        this.running = true;
        REQUEST_WORK.execute(this::run);
    }

    public void stop() {
        try {
            this.running = false;
            serverSocketChannel.close();
            REQUEST_WORK.shutdown();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void run() {
        try {
            //打开ServerSocketChannel通道
            serverSocketChannel = ServerSocketChannel.open();
            // 绑定端口
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            // 构建选择器
            Selector selector = Selector.open();
            // 生命为非阻塞通道
            serverSocketChannel.configureBlocking(false);
            // 注册事件：链接就绪
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                if (!running) {
                    return;
                }

                selector.select();
                Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
                SelectionKey key;
                while (selectedKeys.hasNext()) {
                    key = selectedKeys.next();
                    selectedKeys.remove();
                    try {
                        if (!key.isValid()) {
                            // 当前键被取消
                            continue;
                        }

                        if (key.isAcceptable()) {
                            accept(key);
                        }

                        if (key.isReadable()) {
                            read(key);
                        }

                        if (key.isWritable()) {
                            write(key);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void accept(SelectionKey key) {
        try {
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(key.selector(), SelectionKey.OP_READ);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private com.http.nio.core.Request read(SelectionKey key) {
        com.http.nio.core.Request request = null;
        SocketChannel socketChannel = (SocketChannel) key.channel();

        try {
            int readNum = socketChannel.read(readBuffer);
            if (readNum == -1) {
                socketChannel.close();
                key.cancel();
                return null;
            }
            readBuffer.flip();
            byte[] buffers = new byte[readBuffer.limit()];
            readBuffer.get(buffers);
            readBuffer.clear();
            request = HttpParser.decodeReq(buffers);
            REQUEST_WORK.execute(new RequestWorker(request, key, cloudService));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return request;
    }

    private void write(SelectionKey key) throws IOException {
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        if (buffer == null || !buffer.hasRemaining()) {
            return;
        }

        SocketChannel socketChannel = (SocketChannel) key.channel();
        socketChannel.write(buffer);
        if (!buffer.hasRemaining()) {
            key.interestOps(SelectionKey.OP_READ);
            buffer.clear();
            // help GC
            key.attach(null);
        }
        socketChannel.close();
    }

    public static void main(String[] args) {
        NioServer nioServer = NioServer.getServerInstance(
                new ServerConfig(
                        8888,
                        new StaticViewServlet()
                )
        );
        nioServer.start();
    }
}
